Skip to main content

Data Fetching

Use the useFetch hook from the Blotch Library whenever your Widget needs data from an API.

import { useFetch } from "~lib/use-fetch";

type Weather = {
readonly temperature: number;
};

const Widget = () => {
const { data } = useFetch<Weather>("https://api.example.com/weather");

return <div className="text-6xl">{data.temperature}°C</div>;
};

Your Widget waits for the data before it is drawn, so data is always there. There is no loading state to check.

tip

Use useFetch instead of the browser's fetch. Requests go through Blotch's servers, so APIs that block requests from a browser still work.

Typing the response​

Pass the shape of the response as the type parameter, and data gets full auto-complete:

type Quote = {
readonly text: string;
readonly author: string;
};

const { data } = useFetch<Quote[]>("https://api.example.com/quotes");

data[0].author; // string
tip

QuickType turns a sample JSON response into a TypeScript type you can paste into your Widget.

Using Inputs​

Read your Inputs with useInputs and build them into the URL. Wrap anything a user types in encodeURIComponent, so spaces and symbols survive:

import { useFetch } from "~lib/use-fetch";
import { useInputs } from "~lib/use-inputs";

const Widget = () => {
const { CITY } = useInputs();

const { data } = useFetch<Weather>(
`https://api.example.com/weather?city=${encodeURIComponent(CITY)}`,
);

return <div>{data.temperature}°C</div>;
};

API keys​

Never write an API key into your Widget code, because Widget code is public. Store it as a Private Input and send it in a header:

const { API_KEY } = useInputs();

const { data } = useFetch<Weather>("https://api.example.com/weather", {
headers: { "X-Api-Key": API_KEY },
});

For services you sign in to with OAuth, such as Google or Spotify, use an Integration instead. useIntegration gives you an access token to send as an Authorization header.

Sending data​

The second argument takes the same options as the browser's fetch, such as method, headers and body:

const { data } = useFetch<SearchResult>("https://api.example.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "weather" }),
});

body can be text, such as JSON, or URLSearchParams or FormData to send a form. Files can't be sent.

Keep each request the same​

Your Widget code can run several times while it waits for data, and each response only goes back to the exact request that asked for it. So build the same URL, headers and body every time.

A random ID or the current time comes out different on every run, so its response is never used. After a few tries Blotch stops asking and shows an error in place of your Widget. Make such a value once, outside your Widget:

const DEVICE_ID = Math.random().toString(36).slice(2);

const Widget = () => {
const { data } = useFetch<Session>("https://api.example.com/sessions", {
method: "POST",
body: JSON.stringify({ deviceId: DEVICE_ID }),
});

return <div>Signed in as {data.name}</div>;
};

useState won't keep it either, because React starts your Widget's state over while it waits for its first data.

One request after another​

Because data is always there, a second request can use the first one's result:

const { data: user } = useFetch<User>("https://api.example.com/me");
const { data: posts } = useFetch<Post[]>(
`https://api.example.com/users/${user.id}/posts`,
);

Requests run one after another, so each one adds to how long your Widget takes to prepare. Fetch only what you display.

Caching​

Pass proxyCache with a ttl in milliseconds, and Blotch keeps the response for that long:

const { data } = useFetch<Weather>("https://api.example.com/weather", {
proxyCache: { ttl: 30 * 60 * 1000 }, // 30 minutes
});

Until the ttl runs out, every identical request, from any Frame, gets the saved response instead of asking the API again. A request is identical when its URL, method, headers and body all match, so every Frame showing London's weather shares one response and Paris gets its own.

Use it when the data changes slowly, such as weather or headlines, or when the API is slow, rate-limited or charges per request. Leave it off for data that must always be current.

  • Private requests are never cached. A request with an Authorization, Proxy-Authorization, Cookie or X-Api-Key header always goes to the API, because its response may belong to one person. A key in the URL can't be spotted, so only cache responses that are the same for everyone.
  • Errors are cached too. A shorter ttl recovers from a bad response sooner.
  • The cache can't be cleared early, and a very long ttl is shortened to the maximum Blotch allows.

Errors​

data holds whatever the API sent back, even an error such as 404 Not Found. When the API has an error format, check for it:

type WeatherResponse =
| { readonly temperature: number }
| { readonly error: { readonly message: string } };

const Widget = () => {
const { data } = useFetch<WeatherResponse>("https://api.example.com/weather");

if ("error" in data) {
return <div>Weather unavailable: {data.error.message}</div>;
}

return <div>{data.temperature}°C</div>;
};

If the request can't be made at all, for example because the API is down, Blotch shows the error in place of your Widget. You don't need to handle this yourself.

Retrying​

Some APIs answer "still working on it" and expect you to ask again. Give retry a condition that returns true for those responses, and useFetch waits and asks again, up to count times:

const { data } = useFetch<Report>("https://api.example.com/report", {
retry: {
condition: (report) => report.status === "pending",
count: 5,
},
});

Response formats​

  • JSON is parsed for you.
  • XML, RSS and Atom responses that start with <?xml become plain objects. Elements become keys, and attributes become keys starting with @_, so <rss version="2.0"> becomes data.rss["@_version"].
  • Anything else, such as plain text or HTML, arrives as a string.

If an XML API's responses don't start with <?xml, set output: "force" to convert them anyway. Set output: "pass-through" to keep XML as text.

Reference​

import { useFetch } from "~lib/use-fetch";

const { data } = useFetch<T>(url, options);
ParameterTypeRequiredDescription
urlstringYesThe URL to request.
optionsOptions<T>NoAny fetch option, plus these:
OptionTypeDefaultDescription
proxyCache.ttlnumberNot cachedHow long to keep the response, in milliseconds. See Caching.
retry.condition(data: T) => booleanNo retriesReturn true to ask again. See Retrying.
retry.countnumber3The most times to ask again.
output"best-effort" | "force" | "pass-through""best-effort"How XML is converted. See Response formats.

useFetch returns data, the response body typed as T.